﻿// Arrange the Bulls POJ - 2441.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>


using namespace std;

//https://vjudge.csgrandeur.cn/problem/POJ-2441

/*
描述：
约翰逊农民的公牛非常喜欢打篮球。但他们中没有一个愿意和其他公牛一起打篮球，因为他们认为其他公牛都很弱。
约翰逊农民有N头奶牛（我们将奶牛从1到N编号）和M个谷仓（我们将谷仓从1到M编号），这是他的公牛们的篮球场地。
然而，他的公牛们都非常挑剔，他们只喜欢在一些特定的谷仓里打球，并且不想与其他公牛分享同一个谷仓。

所以对于约翰逊农民来说，安排他的公牛是很困难的，他希望你能帮助他。当然，找到一个解决方案很容易，但是你的任务是找出有多少种解决方案。

你应该知道，一个解决方案是指每头公牛都可以在它喜欢的谷仓里打篮球，并且没有两头公牛共用一个谷仓。

为了简化问题，假设解决方案的数量不会超过10000000。
输入：
输入的第一行包含两个整数N和M（1 <= N <= 20，1 <= M <= 20）。接下来是N行。
第i行首先包含一个整数P（1 <= P <= M），表示奶牛i喜欢打篮球的谷仓数量。然后是P个整数，表示这P个谷仓的编号。

输出：
在单独的一行中打印一个整数，即解决方案的数量。

示例输入：
3 4
2 1 4
2 1 3
2 2 4

示例输出：
4
*/

const int N = 21;
struct COW {
	int id;
	vector<int> v;
}cows[N];

int n, m, t;

int dp[2][1<<20];

void solve() {
	dp[0][0] = 1;
	int ans = 0;
	int prev = 0; int curr = 1;
	for (int i = 1; i <= n; i++) {
		memset(dp[curr], 0, sizeof dp[curr][0] * (1 << 20));
		for (int j = 0; j < cows[i].v.size(); j++) {
			int pos = 1 << cows[i].v[j];
			for (int state = 0; state < 1 << m; state++) {
				if ((state & pos) == 0) {
					dp[curr][state | pos] += dp[prev][state];
				}
			}
		}
		swap(curr, prev);
	}

	for (int i = 0; i < 1 << m; i++) {
		ans += dp[prev][i];
	}
	cout << ans << endl;
}

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> t;
		cows[i].id = i;
		for (int j = 0; j < t; j++) {
			int a; cin >> a;
			cows[i].v.push_back(a-1);
		}
	}

	solve();

	return 0;
}
 